במדריך נלמד על האפשרויות ש-File API של HTML5 מציע.
כידוע, ב-HTML5 יש הרבה דברים חדשים ומגניבים, וביניהם File API.
FIle API, כמו שניתן להסיק מהשם, זהו ממשק תכנות יישומים הנועד לעבודה עם קבצים. במדריך הזה נראה איך ניתן לשלב את File API במערכת העלאת תמונות.
היתרונות של FIle API
• אפשרות לבחור מספר קבצים דרך שדה הבחירה הרגיל.
• הוא אינו תלוי בשום פלאגין חיצוני.
• אפשרות שליטה על תהליך העלאה והצגת מידע עליו, כלומר ניתן ליצור "פס מצב".
• אפשרות לקרוא קובץ ולגלות את גודלו עוד לפני תחילת העלאה.
• אפשרות להשתמש בממשק Drag and Drop בכדי לבחור את הקבצים, כלומר, ניתן לגרור את הקבצים הישר משולחן העבודה.
שני החסרונות הגדולים של FIle API
הוא נתמך רק בדפדפן Firefox 3.6+ ובדפדפן Chrome 6.0+. בנוסף לכך, בעת שימוש בו, קבצים גדולים נטענים לתוך הזיכרון, מה שעלול לתקוע את הדפדפן וכמובן לתפוס מקום נאה בזכרון. FormData יותר חכם במקרה כזה.
נתחיל בקוד html:
...
<div>
<input type="file" name="file" id="upload_input" multiple="true" />
</div>
<div id="imageContainer">
<ul id="imageList"></ul>
</div>
...
<div>
<input type="file" name="file" id="upload_input" multiple="true" />
</div>
<div id="imageContainer">
<ul id="imageList"></ul>
</div>
...
אין פה משהו מסובך. בסך הכול יצרנו שדה רגיל מסוג file וקונטיינר (מכולה) שלתוכו נגרור את הקבצים. חשוב לציין שהמאפיין multiple עם הערך true, כמו במקרה הזה, מאפשר לבחור יותר מקובץ אחד בבחירה הרגילה.
הגיע הזמן לקצת JavaScript. השתמשתי ב-jQuery בכדי להקל על עבודה עם ה-DOM, וזה גם נראה יפה יותר ומסודר יותר, לא?
בהתחלה נשמור במשתנים את הקישור לאלמנטים שבדף. לאחר מכן נגדיר את המטפלים באירועים של שדה ההעלאה הרגיל ושל הקונטיינר שלתוכו נגרור את הקבצים.
var fInput = $("#upload_input");
var imgList = $("ul#imageList");
var dropArea = $("#imageContainer");
// עדכון פס המצב
function updateProgressBar(bar, value) {
var barWidth = bar.width();
var bVal = -barWidth + (value * (width / 100));
bar.attr('rel', value).css('background-position', bVal + 'px center').text(value + '%');
}
// טיפול באירוע של בחירת הקבצים דרך שדה סטנדרטי
fInput.bind({
change: function() {
showImgs(this.files);
}
});
dropArea.bind({
dragenter: function() {
$(this).addClass('highlightedBorder'); // נוסיף איזה בורדר יפה
return false;
},
dragover: function() { return false; },
dragleave: function() {
$(this).removeClass('highlightedBorder'); // הוא כבר לא יפה
return false;
},
drop: function(e) {
var dTransfer = e.originalEvent.dataTransfer;
showImgs(dTransfer.files);
return false;
}
});
// טיפול בכפתור העלאה
$("#uploadMyImages").click(function() {
imgList.find('li').each(function() {
var upItem = this;
var pBar = $(upItem).find('.progress');
new uploaderObject({
file: upItem.file,
url: './uploader.php',
fieldName: 'myPic',
onprogress: function(percents) {
updateProgress(pBar, percents);
},
oncomplete: function(done, data) {
if(done) {
updateProgress(pBar, 100);
}
}
});
});
});
// בדיקה האם הדפדפן שלכם גרוע
if(window.FileReader == null) {
log("הדפדפן שבו אתה משתמש לא תומך File API");
}
var imgList = $("ul#imageList");
var dropArea = $("#imageContainer");
// עדכון פס המצב
function updateProgressBar(bar, value) {
var barWidth = bar.width();
var bVal = -barWidth + (value * (width / 100));
bar.attr('rel', value).css('background-position', bVal + 'px center').text(value + '%');
}
// טיפול באירוע של בחירת הקבצים דרך שדה סטנדרטי
fInput.bind({
change: function() {
showImgs(this.files);
}
});
dropArea.bind({
dragenter: function() {
$(this).addClass('highlightedBorder'); // נוסיף איזה בורדר יפה
return false;
},
dragover: function() { return false; },
dragleave: function() {
$(this).removeClass('highlightedBorder'); // הוא כבר לא יפה
return false;
},
drop: function(e) {
var dTransfer = e.originalEvent.dataTransfer;
showImgs(dTransfer.files);
return false;
}
});
// טיפול בכפתור העלאה
$("#uploadMyImages").click(function() {
imgList.find('li').each(function() {
var upItem = this;
var pBar = $(upItem).find('.progress');
new uploaderObject({
file: upItem.file,
url: './uploader.php',
fieldName: 'myPic',
onprogress: function(percents) {
updateProgress(pBar, percents);
},
oncomplete: function(done, data) {
if(done) {
updateProgress(pBar, 100);
}
}
});
});
});
// בדיקה האם הדפדפן שלכם גרוע
if(window.FileReader == null) {
log("הדפדפן שבו אתה משתמש לא תומך File API");
}
אנחנו מקבלים כאן גישה לאובייקט FileList, שהוא בעצם מערך של אובייקטים מסוג File. את המערך הזה אנחנו מעבירים לפונקציה ()showImgs:
// הצגת התמונות שבחרנו ויצירת מיניאטורות
function showImgs(files) {
var imgType = /image.*/;
var num = 0;
$.each(files, function(i, file)) {
// סינון תמונות
if(!file.type.match(imgType)) return true;
num++;
// ניצור אלמנט של פריט רשימה ונשים בתוכו את השם, מיניאטורה ופס מצב
var li = $('<li/>').appendTo(imgList);
$('<div/>').text(file.name).appendTo(li);
var img = $('<img/>').appendTo(li);
$('<div/>').addClass('progress').attr('rel', '0').text('0%').appendTo(li);
var readerObj = new FileReader();
readerObj.onload = (function(ltImage) {
return function(e) {
ltImage.attr('src', e.target.result);
ltImage.attr('width', 200);
};
})(img);
reader.readAsDataUrl(file);
});
}
function showImgs(files) {
var imgType = /image.*/;
var num = 0;
$.each(files, function(i, file)) {
// סינון תמונות
if(!file.type.match(imgType)) return true;
num++;
// ניצור אלמנט של פריט רשימה ונשים בתוכו את השם, מיניאטורה ופס מצב
var li = $('<li/>').appendTo(imgList);
$('<div/>').text(file.name).appendTo(li);
var img = $('<img/>').appendTo(li);
$('<div/>').addClass('progress').attr('rel', '0').text('0%').appendTo(li);
var readerObj = new FileReader();
readerObj.onload = (function(ltImage) {
return function(e) {
ltImage.attr('src', e.target.result);
ltImage.attr('width', 200);
};
})(img);
reader.readAsDataUrl(file);
});
}
האובייקט File מכיל מטא-דטא אודות הקובץ, כמו השם שלו, גודלו וסוגו (תקן MIME לדוגמה) במאפיינים name, size, type בהתאמה. בכדי לגשת לתוכן הקובץ עצמו, קיים אובייקט מיוחד הנקרא FileReader.
בפונקציה ()showImgs אנחנו עוברים על המערך ומסננים את הקבצים שהם אינם תמונות. לאחר מכן, עבור כל תמונה אנחנו יוצרים אלמנט li חדש, ולתוכו מכניסים את האלמנט img שהינו ריק לבינתיים. לאחר מכן, אנו יוצרים עותק FileReader ובשבילו מוגדר מטפל באירוע onload שבו הנתונים מועברים ישר למאפיין src של האלמנט img שיצרנו מקודם. מתודה ()readAsDataURL של האובייקט FileReader מקבלת כפרמטר את האובייקט File ומפעילה קריאת מידע מתוכו. כתוצאה מכך, לכל התמונות שגררנו אל תוך הדפדפן או שבחרנו דרך השדה הסטנדרטי, אנו רואים מיניאטורה.
נשאר רק להעלות לשרת את כל הקבצים שבחרנו. לשם כך, ניצור כפתור או לחלופין קישור, שבלחיצה עליו נפעיל מנגנון שירוץ על כל האלמנטים של li שיצרנו, יקרא את המאפיינים שלהם ויעבירם לאובייקט uploaderObject.
var uploaderObject = function(params) {
if(!params.file || !params.url) {
return false;
}
this.xhr = new XMLHttpRequest();
this.reader = new FileReader();
this.progress = 0;
this.uploaded = false;
this.successful = false;
this.lastError = false;
var self = this;
self.reader.onload = function() {
self.xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
self.progress = (e.loaded * 100) / e.total;
if(params.onprogress instanceof Function) {
params.onprogress.call(self, Math.round(self.progress));
}
}
}, false);
self.xhr.upload.addEventListener("load", function(){
self.progress = 100;
self.uploaded = true;
}, false);
self.xhr.upload.addEventListener("error", function(){
self.lastError = {
code: 1,
text: 'Error uploading on server'
};
}, false);
self.xhr.onreadystatechange = function () {
var callbackDefined = params.oncomplete instanceof Function;
if (this.readyState == 4) {
if(this.status == 200) {
if(!self.uploaded) {
if(callbackDefined) {
params.oncomplete.call(self, false);
}
} else {
self.successful = true;
if(callbackDefined) {
params.oncomplete.call(self, true, this.responseText);
}
}
} else {
self.lastError = {
code: this.status,
text: 'HTTP response code is not OK ('+this.status+')'
};
if(callbackDefined) {
params.oncomplete.call(self, false);
}
}
}
};
self.xhr.open("POST", params.url);
var boundary = "xxxxxxxxx";
self.xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary="+boundary);
self.xhr.setRequestHeader("Cache-Control", "no-cache");
var body = "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name='"+(params.fieldName || 'file')+"'; filename='" + params.file.name + "'\r\n";
body += "Content-Type: " + params.file.type + "\r\n\r\n";
body += self.reader.result + "\r\n";
body += "--" + boundary + "--";
if(self.xhr.sendAsBinary) {
self.xhr.sendAsBinary(body);
} else {
self.xhr.send(body);
}
};
self.reader.readAsBinaryString(params.file);
};
if(!params.file || !params.url) {
return false;
}
this.xhr = new XMLHttpRequest();
this.reader = new FileReader();
this.progress = 0;
this.uploaded = false;
this.successful = false;
this.lastError = false;
var self = this;
self.reader.onload = function() {
self.xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
self.progress = (e.loaded * 100) / e.total;
if(params.onprogress instanceof Function) {
params.onprogress.call(self, Math.round(self.progress));
}
}
}, false);
self.xhr.upload.addEventListener("load", function(){
self.progress = 100;
self.uploaded = true;
}, false);
self.xhr.upload.addEventListener("error", function(){
self.lastError = {
code: 1,
text: 'Error uploading on server'
};
}, false);
self.xhr.onreadystatechange = function () {
var callbackDefined = params.oncomplete instanceof Function;
if (this.readyState == 4) {
if(this.status == 200) {
if(!self.uploaded) {
if(callbackDefined) {
params.oncomplete.call(self, false);
}
} else {
self.successful = true;
if(callbackDefined) {
params.oncomplete.call(self, true, this.responseText);
}
}
} else {
self.lastError = {
code: this.status,
text: 'HTTP response code is not OK ('+this.status+')'
};
if(callbackDefined) {
params.oncomplete.call(self, false);
}
}
}
};
self.xhr.open("POST", params.url);
var boundary = "xxxxxxxxx";
self.xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary="+boundary);
self.xhr.setRequestHeader("Cache-Control", "no-cache");
var body = "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name='"+(params.fieldName || 'file')+"'; filename='" + params.file.name + "'\r\n";
body += "Content-Type: " + params.file.type + "\r\n\r\n";
body += self.reader.result + "\r\n";
body += "--" + boundary + "--";
if(self.xhr.sendAsBinary) {
self.xhr.sendAsBinary(body);
} else {
self.xhr.send(body);
}
};
self.reader.readAsBinaryString(params.file);
};
כאן נוצר עותק של האובייקט FileReader, בדיוק כמו קודם. אליו אנחנו מצרפים את המטפל באירוע onload שבתוכו נוצר אובייקט XMLHttpRequest. בגרסה השנייה של XMLHttpRequest נוסף המאפיין upload, שמכיל אובייקט העלאה, שיכול לעבד ולטפל באירועים progress, load ו-error. לאחר מכן, נצרף את המטפל באירוע סיום הבקשה, שנשלחת ל-request עצמו. נוסיף 2 כותרות ונגדיר את תוכן הבקשה תוך כדי קריאה של נתונים מהמאפיין result של האובייקט FileReader. לאחר מכן, ההעלאה מתחילה. חשוב לציין שלפי הספציפיקציה של W3C, המתודה ()send של האובייקט XMLHttpRequest יכולה לקבל כפרמטר נתונים בינארים, ואכן כך זה ממומש ב-Chrome, אך לא ב-Firefox. הבחורים החליטו לעשות את זה בדרך שלהם (like a boss), דרך מתודה ()sendAsBinary. לכן צריך לבדוק האם המתודה הזאת מוגדרת באובייקט של הפנייה, ואם כן, אז להשתמש בה.
בשרת מקבלים את התמונה בדרך הבאה:
$file = $_FILES['myPic'];
וזהו זה. :)
תגובות לכתבה:
בעבר ניסיתי לגרום לכרום לקרוא קובץ מהמחשב הלוקאלי, לפענח אותו ולעשות איתו משהו אבל זה בלתי אפשרי עקב הגבלות בטיחות שכרום מפעילה בשביל שאתרים לא יגנבו למשתמשים קבצים. אז זה לא אפשרי למעט גרסאות אחרונות של כרום.
היום זה יותר פשוט בהנחה שהמשתמש בעצמו בוחר את הקובץ דרך שדה בחירה כזה והנונים מגיעים אל fileapi, ככה שאפשר לקרוא את תוכן הקובץ עצמו ולעשות איתו מה שבה לכם. למשל לעשות באתר הזדהות לפי קבצים או עוד משהו משוכע אחר.
נכון שהטכנולוגיה הזו עוד לא גמורה סופית וכנראה במייקרוסופט בכלל לא שמעו עליה, אבל לעשות פעולות על תוכן הקובץ עד עכשיו היה בלתי אפשרי בצד של הלקוח.
לשלוח ב ajax היה אפשר גם לפני, אבל לקבל פרוגרס באר מזה היה הרבה יותר קשה.
תודה על המריך, ctulhuil, ננסה את זה בהזדמנות הקרובה.
מדריך מעולה. נוסח מצוין ומוסבר היטב, וגם הקוד קריא :)
תודה רבה.
אחלה מדריך!
תודה
כל הכבוד על המדריך... בהחלט אנסה לעבוד עם FILE API
משהו קטן שהייתי מוסיף זה בתחילת המדריך קישור לדמו קטן שמראה את זה בפעולה
דמו יש אבל הוא ממומש בצורה אחרת. למי שמעוניין, קוד שלם ניתן לראות ברפו:
https://github.com/cthulhu25/File-API-based-Image-Upload